1 /** 2 * 测试用例库文件,提供如event mock、iframe封装等各种常用功能 部分方法来源于YUI测试框架 3 * @namespace UserAction 4 * @name UserAction 5 */ 6 UserAction = { 7 beforedispatch:null, 8 /** 9 * 判断是否为function 10 * @grammar isf 11 * @param {Any} value 判断的对象 12 * @returns {Boolean} 真或假 13 */ 14 isf /* is function ? */:function ( value ) { 15 return value && (typeof value == 'function'); 16 }, 17 /** 18 * 判断是否为布尔类型 19 * @grammar isb 20 * @param {Any} value 判断的对象 21 * @returns {Boolean} 真或假 22 */ 23 isb /* is boolean? */:function ( value ) { 24 return value && (typeof value == 'boolean'); 25 }, 26 /** 27 * 判断是否为对象 28 * @grammar iso 29 * @param {Any} value 判断的对象 30 * @returns {Boolean} 真或假 31 */ 32 iso /* is object? */:function ( value ) { 33 return value && (typeof value == 'object'); 34 }, 35 /** 36 * 判断是否为字符串 37 * @grammar isos 38 * @param {Any} value 判断的对象 39 * @returns {Boolean} 真或假 40 */ 41 iss /* is string? */:function ( value ) { 42 return value && (typeof value == 'string'); 43 }, 44 /** 45 * 判断是否为数字 46 * @grammar isn 47 * @param {Any} value 判断的对象 48 * @returns {Boolean} 真或假 49 */ 50 isn /* is number? */:function ( value ) { 51 return value && (typeof value == 'number'); 52 }, 53 // -------------------------------------------------------------------------- 54 // Generic event methods 55 // -------------------------------------------------------------------------- 56 57 /** 58 * 根据给定的事件信息模拟键盘事件,并冒泡到产生这个事件的对象上 59 * @param {HTMLElement} 被触发事件的元素. 60 * @param {String} type 事件类型,如: keyup, keydown, 或 keypress. 61 * @param {Boolean} bubbles (Optional) 事件是否允许冒泡,默认为true 62 * @param {Boolean} cancelable (Optional) 事件是否允许通过preventDefault取消,默认是true. 63 * @param {Window} view (Optional) 触发事件的window容器,默认就是window 64 * @param {Boolean} ctrlKey (Optional) 触发事件时是否触发ctrl键的事件,默认为false. 65 * @param {Boolean} altKey (Optional) I触发事件时是否触发atl键的事件,默认为false 66 * @param {Boolean} shiftKey (Optional) 触发事件时是否触发shift键的事件,默认为false. 67 * @param {Boolean} metaKey (Optional) 触发事件时是否触发meta键的事件,默认为false. 68 * @param {int} keyCode (Optional) 触发键盘事件时按下的键值(keycode),默认为0. 69 * @param {int} charCode (Optional) 触发键盘事件时按下的Unicode值(charCode),默认为0. 70 */ 71 simulateKeyEvent:function ( target /* :HTMLElement */, type /* :String */, bubbles /* :Boolean */, cancelable /* :Boolean */, view /* :Window */, ctrlKey /* :Boolean */, altKey /* :Boolean */, shiftKey /* :Boolean */, metaKey /* :Boolean */, keyCode /* :int */, charCode /* :int */ ) /* :Void */ { 72 // check target 73 target = typeof target == 'string' ? document.getElementById( target ) 74 : target; 75 if ( !target ) { 76 throw new Error( "simulateKeyEvent(): Invalid target." ); 77 } 78 79 // check event type 80 if ( typeof type == 'string' ) { 81 type = type.toLowerCase(); 82 switch ( type ) { 83 case "keyup": 84 case "keydown": 85 case "keypress": 86 break; 87 case "textevent": // DOM Level 3 88 type = "keypress"; 89 break; 90 // @TODO was the fallthrough intentional, if so throw error 91 default: 92 throw new Error( "simulateKeyEvent(): Event type '" + type 93 + "' not supported." ); 94 } 95 } else { 96 throw new Error( "simulateKeyEvent(): Event type must be a string." ); 97 } 98 99 // setup default values 100 if ( !this.isb( bubbles ) ) { 101 bubbles = true; // all key events bubble 102 } 103 if ( !this.isb( cancelable ) ) { 104 cancelable = true; // all key events can be cancelled 105 } 106 if ( !this.iso( view ) ) { 107 view = window; // view is typically window 108 } 109 if ( !this.isb( ctrlKey ) ) { 110 ctrlKey = false; 111 } 112 if ( !this.isb( typeof altKey == 'boolean' ) ) { 113 altKey = false; 114 } 115 if ( !this.isb( shiftKey ) ) { 116 shiftKey = false; 117 } 118 if ( !this.isb( metaKey ) ) { 119 metaKey = false; 120 } 121 if ( !(typeof keyCode == 'number') ) { 122 keyCode = 0; 123 } 124 if ( !(typeof charCode == 'number') ) { 125 charCode = 0; 126 } 127 128 // try to create a mouse event 129 var customEvent /* :MouseEvent */ = null; 130 131 // check for DOM-compliant browsers first 132 if ( this.isf( document.createEvent ) ) { 133 134 try { 135 136 // try to create key event 137 customEvent = document.createEvent( "KeyEvents" ); 138 139 /* 140 * Interesting problem: Firefox implemented a non-standard 141 * version of initKeyEvent() based on DOM Level 2 specs. Key 142 * event was removed from DOM Level 2 and re-introduced in DOM 143 * Level 3 with a different interface. Firefox is the only 144 * browser with any implementation of Key Events, so for now, 145 * assume it's Firefox if the above line doesn't error. 146 */ 147 // TODO: Decipher between Firefox's implementation and a correct 148 // one. 149 customEvent.initKeyEvent( type, bubbles, cancelable, view, 150 ctrlKey, altKey, shiftKey, metaKey, keyCode, charCode ); 151 152 } catch ( ex /* :Error */ ) { 153 154 /* 155 * If it got here, that means key events aren't officially 156 * supported. Safari/WebKit is a real problem now. WebKit 522 157 * won't let you set keyCode, charCode, or other properties if 158 * you use a UIEvent, so we first must try to create a generic 159 * event. The fun part is that this will throw an error on 160 * Safari 2.x. The end result is that we need another 161 * try...catch statement just to deal with this mess. 162 */ 163 try { 164 165 // try to create generic event - will fail in Safari 2.x 166 customEvent = document.createEvent( "Events" ); 167 168 } catch ( uierror /* :Error */ ) { 169 170 // the above failed, so create a UIEvent for Safari 2.x 171 customEvent = document.createEvent( "UIEvents" ); 172 173 } finally { 174 175 customEvent.initEvent( type, bubbles, cancelable ); 176 177 // initialize 178 customEvent.view = view; 179 customEvent.altKey = altKey; 180 customEvent.ctrlKey = ctrlKey; 181 customEvent.shiftKey = shiftKey; 182 customEvent.metaKey = metaKey; 183 customEvent.keyCode = keyCode; 184 customEvent.charCode = charCode; 185 186 } 187 188 } 189 190 // before dispatch 191 if ( this.beforedispatch && typeof this.beforedispatch == 'function' ) 192 this.beforedispatch( customEvent ); 193 this.beforedispatch = null; 194 195 // fire the event 196 target.dispatchEvent( customEvent ); 197 198 } else if ( this.iso( document.createEventObject ) ) { // IE 199 200 // create an IE event object 201 customEvent = document.createEventObject(); 202 203 // assign available properties 204 customEvent.bubbles = bubbles; 205 customEvent.cancelable = cancelable; 206 customEvent.view = view; 207 customEvent.ctrlKey = ctrlKey; 208 customEvent.altKey = altKey; 209 customEvent.shiftKey = shiftKey; 210 customEvent.metaKey = metaKey; 211 212 /* 213 * IE doesn't support charCode explicitly. CharCode should take 214 * precedence over any keyCode value for accurate representation. 215 */ 216 customEvent.keyCode = (charCode > 0) ? charCode : keyCode; 217 218 // before dispatch 219 if ( this.beforedispatch && typeof this.beforedispatch == 'function' ) 220 this.beforedispatch( customEvent ); 221 this.beforedispatch = null; 222 223 // fire the event 224 target.fireEvent( "on" + type, customEvent ); 225 226 } else { 227 throw new Error( 228 "simulateKeyEvent(): No event simulation framework present." ); 229 } 230 231 this.beforedispatch = null; 232 }, 233 234 /** 235 * 模拟鼠标事件 236 * @param {HTMLElement} target 被触发事件的元素. 237 * @param {String} type 事件类型,如 click, dblclick, mousedown, mouseup, mouseout, mouseover, 和 mousemove 238 * @param {Boolean} bubbles (Optional) 事件是否允许冒泡,默认为true 239 * @param {Boolean} cancelable (Optional) 事件是否允许通过preventDefault取消,默认是true. 240 * @param {Window} view (Optional) 触发事件的window容器,默认就是window 241 * @param {int} detail (Optional) 鼠标按钮使用的次数,默认是1 242 * @param {int} screenX (Optional) 事件触发时相对屏幕上的x坐标,默认是0 243 * @param {int} screenY (Optional) 事件触发时相对屏幕上的Y坐标,默认是0 244 * @param {int} clientX (Optional) 事件触发时相对client上的X坐标,默认是0 245 * @param {int} clientY (Optional) 事件触发时相对client上的Y坐标,默认是0 246 * @param {Boolean} ctrlKey (Optional) 触发事件时是否触发ctrl键的事件,默认为false. 247 * @param {Boolean} altKey (Optional) I触发事件时是否触发atl键的事件,默认为false 248 * @param {Boolean} shiftKey (Optional) 触发事件时是否触发shift键的事件,默认为false. 249 * @param {Boolean} metaKey (Optional) 触发事件时是否触发meta键的事件,默认为false. 250 * @param {int} button (Optional) 鼠标按下的键,0指左键,1中间键,2指右键,默认是0. 251 * @param {HTMLElement} relatedTarget (Optional) 事件相关的目标元素,mouseout事件中指鼠标移动到的元素,mouseover指鼠标从哪一个元素移动过来,其他事件忽略这个参数,默认是null 252 */ 253 simulateMouseEvent:function ( target /* :HTMLElement */, type /* :String */, bubbles /* :Boolean */, cancelable /* :Boolean */, view /* :Window */, detail /* :int */, screenX /* :int */, screenY /* :int */, clientX /* :int */, clientY /* :int */, ctrlKey /* :Boolean */, altKey /* :Boolean */, shiftKey /* :Boolean */, metaKey /* :Boolean */, button /* :int */, relatedTarget /* :HTMLElement */ ) /* :Void */ { 254 255 // check target 256 target = typeof target == 'string' ? document.getElementById( target ) 257 : target; 258 if ( !target ) { 259 throw new Error( "simulateMouseEvent(): Invalid target." ); 260 } 261 262 // check event type 263 if ( this.iss( type ) ) { 264 type = type.toLowerCase(); 265 switch ( type ) { 266 case "mouseover": 267 case "mouseout": 268 case "mousedown": 269 case "mouseup": 270 case "click": 271 case "dblclick": 272 case "mousemove": 273 case "mouseenter":// 非标准支持,仅为测试提供,该项仅IE下work 274 case "mouseleave": 275 break; 276 default: 277 throw new Error( "simulateMouseEvent(): Event type '" + type 278 + "' not supported." ); 279 } 280 } else { 281 throw new Error( 282 "simulateMouseEvent(): Event type must be a string." ); 283 } 284 285 // setup default values 286 if ( !this.isb( bubbles ) ) { 287 bubbles = true; // all mouse events bubble 288 } 289 if ( !this.isb( cancelable ) ) { 290 cancelable = (type != "mousemove"); // mousemove is the only one 291 // that can't be cancelled 292 } 293 if ( !this.iso( view ) ) { 294 view = window; // view is typically window 295 } 296 if ( !this.isn( detail ) ) { 297 detail = 1; // number of mouse clicks must be at least one 298 } 299 if ( !this.isn( screenX ) ) { 300 screenX = 0; 301 } 302 if ( !this.isn( screenY ) ) { 303 screenY = 0; 304 } 305 if ( !this.isn( clientX ) ) { 306 clientX = 0; 307 } 308 if ( !this.isn( clientY ) ) { 309 clientY = 0; 310 } 311 if ( !this.isb( ctrlKey ) ) { 312 ctrlKey = false; 313 } 314 if ( !this.isb( altKey ) ) { 315 altKey = false; 316 } 317 if ( !this.isb( shiftKey ) ) { 318 shiftKey = false; 319 } 320 if ( !this.isb( metaKey ) ) { 321 metaKey = false; 322 } 323 if ( !this.isn( button ) ) { 324 button = 0; 325 } 326 327 // try to create a mouse event 328 var customEvent /* :MouseEvent */ = null; 329 330 // check for DOM-compliant browsers first 331 if ( this.isf( document.createEvent ) ) { 332 333 customEvent = document.createEvent( "MouseEvents" ); 334 335 // Safari 2.x (WebKit 418) still doesn't implement initMouseEvent() 336 if ( this.browser.ie !== 9 && customEvent.initMouseEvent ) { 337 customEvent.initMouseEvent( type, bubbles, cancelable, view, 338 detail, screenX, screenY, clientX, clientY, ctrlKey, 339 altKey, shiftKey, metaKey, button, relatedTarget ); 340 } else { // Safari 341 342 // the closest thing available in Safari 2.x is UIEvents 343 customEvent = document.createEvent( "UIEvents" ); 344 customEvent.initEvent( type, bubbles, cancelable ); 345 customEvent.view = view; 346 customEvent.detail = detail; 347 customEvent.screenX = screenX; 348 customEvent.screenY = screenY; 349 customEvent.clientX = clientX; 350 customEvent.clientY = clientY; 351 customEvent.ctrlKey = ctrlKey; 352 customEvent.altKey = altKey; 353 customEvent.metaKey = metaKey; 354 customEvent.shiftKey = shiftKey; 355 customEvent.button = button; 356 customEvent.relatedTarget = relatedTarget; 357 } 358 359 /* 360 * Check to see if relatedTarget has been assigned. Firefox versions 361 * less than 2.0 don't allow it to be assigned via initMouseEvent() 362 * and the property is readonly after event creation, so in order to 363 * keep YAHOO.util.getRelatedTarget() working, assign to the IE 364 * proprietary toElement property for mouseout event and fromElement 365 * property for mouseover event. 366 */ 367 if ( relatedTarget && !customEvent.relatedTarget ) { 368 if ( type == "mouseout" ) { 369 customEvent.toElement = relatedTarget; 370 } else if ( type == "mouseover" ) { 371 customEvent.fromElement = relatedTarget; 372 } 373 } 374 375 // before dispatch 376 if ( this.beforedispatch && typeof this.beforedispatch == 'function' ) 377 this.beforedispatch( customEvent ); 378 this.beforedispatch = null; 379 380 // fire the event 381 target.dispatchEvent( customEvent ); 382 383 } else if ( this.iso( document.createEventObject ) ) { // IE 384 385 // create an IE event object 386 customEvent = document.createEventObject(); 387 388 // assign available properties 389 customEvent.bubbles = bubbles; 390 customEvent.cancelable = cancelable; 391 customEvent.view = view; 392 customEvent.detail = detail; 393 customEvent.screenX = screenX; 394 customEvent.screenY = screenY; 395 customEvent.clientX = clientX; 396 customEvent.clientY = clientY; 397 customEvent.ctrlKey = ctrlKey; 398 customEvent.altKey = altKey; 399 customEvent.metaKey = metaKey; 400 customEvent.shiftKey = shiftKey; 401 402 // fix button property for IE's wacky implementation 403 switch ( button ) { 404 case 0: 405 customEvent.button = 1; 406 break; 407 case 1: 408 customEvent.button = 4; 409 break; 410 case 2: 411 // leave as is 412 break; 413 default: 414 customEvent.button = 0; 415 } 416 417 /* 418 * Have to use relatedTarget because IE won't allow assignment to 419 * toElement or fromElement on generic events. This keeps 420 * YAHOO.util.customEvent.getRelatedTarget() functional. 421 */ 422 customEvent.relatedTarget = relatedTarget; 423 424 // before dispatch 425 if ( this.beforedispatch && typeof this.beforedispatch == 'function' ) 426 this.beforedispatch( customEvent ); 427 this.beforedispatch = null; 428 // fire the event 429 target.fireEvent( "on" + type, customEvent ); 430 431 } else { 432 throw new Error( 433 "simulateMouseEvent(): No event simulation framework present." ); 434 } 435 }, 436 437 // -------------------------------------------------------------------------- 438 // Mouse events 439 // -------------------------------------------------------------------------- 440 441 /** 442 * 在某一个元素上模拟鼠标事件 443 * @param {HTMLElement} target 被触发鼠标事件的元素 444 * @param {String} type 事件类型,如: click, dblclick, mousedown, mouseup, mouseout,mouseover, 和 mousemove. 445 * @param {Object} options 额外参数,使用DOM标准名称 446 * @method mouseEvent 447 * @static 448 */ 449 fireMouseEvent:function ( target /* :HTMLElement */, type /* :String */, options /* :Object */ ) /* :Void */ { 450 options = options || {}; 451 this.simulateMouseEvent( target, type, options.bubbles, 452 options.cancelable, options.view, options.detail, 453 options.screenX, options.screenY, options.clientX, 454 options.clientY, options.ctrlKey, options.altKey, 455 options.shiftKey, options.metaKey, options.button, 456 options.relatedTarget ); 457 }, 458 459 /** 460 * 模拟鼠标单击事件 461 * @remark 鼠标单击事件 462 * @param {HTMLElement} 单击的元素对象 463 * @param {Object} 单击事件的可选参数 464 * @method click 465 */ 466 click:function ( target /* :HTMLElement */, options /* :Object */ ) /* :Void */ { 467 this.fireMouseEvent( target, "click", options ); 468 }, 469 470 /** 471 * 模拟鼠标双击事件 472 * @param {HTMLElement}双击的元素对象 473 * @param {Object}双击事件的可选参数 474 * @method dblclick 475 */ 476 dblclick:function ( target /* :HTMLElement */, options /* :Object */ ) /* :Void */ { 477 this.fireMouseEvent( target, "dblclick", options ); 478 }, 479 480 /** 481 * 模拟鼠标左键按下 482 * @param {HTMLElement} target 鼠标单击的对象 483 * @param {Object} options 额外参数,使用DOM标准名称 484 * @method mousedown 485 */ 486 mousedown:function ( target /* :HTMLElement */, options /* Object */ ) /* :Void */ { 487 this.fireMouseEvent( target, "mousedown", options ); 488 }, 489 490 /** 491 * 模拟鼠标移动 492 * @param {HTMLElement} target 鼠标事件的对象 493 * @param {Object} options 额外参数,使用DOM标准名称 494 * @method mousemove 495 */ 496 mousemove:function ( target /* :HTMLElement */, options /* Object */ ) /* :Void */ { 497 this.fireMouseEvent( target, "mousemove", options ); 498 }, 499 500 /**模拟鼠标移出某一个元素事件 501 * @param {HTMLElement} target 鼠标事件的对象 502 * @param {Object} options 额外参数,使用DOM标准名称 503 * @method mouseout 504 */ 505 mouseout:function ( target /* :HTMLElement */, options /* Object */ ) /* :Void */ { 506 this.fireMouseEvent( target, "mouseout", options ); 507 }, 508 509 /**模拟鼠标移动经过某一个元素事件 510 * @param {HTMLElement} target 鼠标事件的对象 511 * @param {Object} options 额外参数,使用DOM标准名称 512 * @method mouseover 513 * @static 514 */ 515 mouseover:function ( target /* :HTMLElement */, options /* Object */ ) /* :Void */ { 516 this.fireMouseEvent( target, "mouseover", options ); 517 }, 518 519 /** 520 * 模拟鼠标左键在某一个元素上弹起 521 * 522 * @param {HTMLElement} target 鼠标事件的对象 523 * @param {Object} options 额外参数,使用DOM标准名称 524 * @method mouseup 525 * @static 526 */ 527 mouseup:function ( target /* :HTMLElement */, options /* Object */ ) /* :Void */ { 528 this.fireMouseEvent( target, "mouseup", options ); 529 }, 530 531 dragto:function ( target, options ) { 532 var me = this; 533 me.mousemove( target, { 534 clientX:options.startX, 535 clientY:options.startY 536 } ); 537 setTimeout( function () { 538 me.mousedown( target, { 539 clientX:options.startX, 540 clientY:options.startY 541 } ); 542 setTimeout( function () { 543 me.mousemove( target, { 544 clientX:options.endX, 545 clientY:options.endY 546 } ); 547 setTimeout( function () { 548 me.mouseup( target, { 549 clientX:options.endX, 550 clientY:options.endY 551 } ); 552 if ( options.callback ) 553 options.callback(); 554 }, options.aftermove || 20 ); 555 }, options.beforemove || 20 ); 556 }, options.beforestart || 50 ); 557 }, 558 559 // -------------------------------------------------------------------------- 560 // Key events 561 // -------------------------------------------------------------------------- 562 563 /** 564 * 触发键盘事件,需要设置keyCode或charCode 565 * @param {String} type 键盘事件类型 ("keyup", "keydown" or "keypress"). 566 * @param {HTMLElement} 目标元素 567 * @param {Object} options 触发事件的参数,设置keyCode或charCode 568 * @method fireKeyEvent 569 * @static 570 */ 571 fireKeyEvent:function ( type /* :String */, target /* :HTMLElement */, options /* :Object */ ) /* :Void */ { 572 options = options || {}; 573 this.simulateKeyEvent( target, type, options.bubbles, 574 options.cancelable, options.view, options.ctrlKey, 575 options.altKey, options.shiftKey, options.metaKey, 576 options.keyCode, options.charCode ); 577 }, 578 579 /** 580 * 模拟键盘按下事件 581 * @param {HTMLElement} target 鼠标事件的对象 582 * @param {Object} options 额外参数,使用DOM标准名称 583 * @method keydown 584 * @static 585 */ 586 keydown:function ( target /* :HTMLElement */, options /* :Object */ ) /* :Void */ { 587 this.fireKeyEvent( "keydown", target, options ); 588 }, 589 590 /** 591 * 模拟键盘按键事件,包括鼠标按下和弹起 592 * @param {HTMLElement} target 鼠标事件的对象 593 * @param {Object} options 额外参数,使用DOM标准名称 594 * @method keypress 595 */ 596 keypress:function ( target /* :HTMLElement */, options /* :Object */ ) /* :Void */ { 597 this.fireKeyEvent( "keypress", target, options ); 598 }, 599 600 /** 601 * 模拟键盘弹起事件 602 * @param {HTMLElement} target 鼠标事件的对象 603 * @param {Object} options 额外参数,使用DOM标准名称 604 */ 605 keyup:function ( target /* :HTMLElement */, options /* Object */ ) /* :Void */ { 606 this.fireKeyEvent( "keyup", target, options ); 607 }, 608 609 /** 610 * 提供iframe扩展支持,用例测试需要独立场景的用例,由于异步支持,通过finish方法触发start 611 * <li>事件绑定在frame上,包括afterfinish和jsloaded 612 * 613 * @param op.win window对象 614 * @param op.nojs 不加载额外js 615 * @param op.ontest 测试步骤 616 * @param op.onbeforestart {Function} 测试启动前处理步骤,默认为QUnit.stop(); 617 * @param op.onafterfinish {Function} 测试完毕执行步骤,默认为QUnit.start() 618 * 619 */ 620 frameExt:function ( op ) { 621 stop(); 622 op = typeof op == 'function' ? { 623 ontest:op 624 } : op; 625 var pw = op.win || window, w, f, url = '', id = typeof op.id == 'undefined' ? 'f' 626 : op.id, fid = 'iframe#' + id; 627 628 op.finish = function () { 629 pw.$( fid ).unbind(); 630 setTimeout( function () { 631 pw.$( 'div#div' + id ).remove(); 632 start(); 633 }, 20 ); 634 }; 635 636 if ( pw.$( fid ).length == 0 ) { 637 /* 添加frame,部分情况下,iframe没有边框,为了可以看到效果,添加一个带边框的div */ 638 pw.$( pw.document.body ).append( '<div id="div' + id + '"></div>' ); 639 pw.$( 'div#div' + id ).append( '<iframe id="' + id + '"></iframe>' ); 640 } 641 op.onafterstart && op.onafterstart( $( 'iframe#f' )[0] ); 642 pw.$( 'script' ).each( function () { 643 if ( this.src && this.src.indexOf( 'import.php' ) >= 0 ) { 644 url = this.src.split( 'import.php' )[1]; 645 } 646 } ); 647 pw.$( fid ).one( 'load', 648 function ( e ) { 649 var w = e.target.contentWindow; 650 var h = setInterval( function () { 651 if ( w.baidu ) {// 等待加载完成,IE6下这地方总出问题 652 clearInterval( h ); 653 op.ontest( w, w.frameElement ); 654 } 655 }, 20 ); 656 // 找到当前操作的iframe,然后call ontest 657 } ).attr( 'src', cpath + 'frame.php' + url ); 658 }, 659 660 /** 661 * 662 * 判断2个数组是否相等 663 * 664 * @param {Array} array1 665 * @param {Array} array1 666 * @returns {Boolean} 真或假 667 */ 668 isEqualArray:function ( array1, array2 ) { 669 if ( '[object Array]' != Object.prototype.toString.call( array1 ) 670 || '[object Array]' != Object.prototype.toString.call( array2 ) ) 671 return (array1 === array2); 672 else if ( array1.length != array2.length ) 673 return false; 674 else { 675 for ( var i in array1 ) { 676 if ( array1[i] != array2[i] ) 677 return false; 678 } 679 return true; 680 } 681 }, 682 683 /* 684 * 685 * 通用数据模块 686 * 687 * @static 688 * 689 */ 690 commonData:{// 针对测试文件的路径而不是UserAction的路径 691 "testdir":'../../', 692 datadir:(function () { 693 return location.href.split( "/test/" )[0] + "/test/tools/data/"; 694 })(), 695 currentPath:function () { 696 var params = location.search.substring( 1 ).split( '&' ); 697 for ( var i = 0; i < params.length; i++ ) { 698 var p = params[i]; 699 if ( p.split( '=' )[0] == 'case' ) { 700 var casepath = p.split( '=' )[1].split( '.' ).join( '/' ); 701 return location.href.split( '/test/' )[0] + '/test/' 702 + casepath.substring( 0, casepath.lastIndexOf( '/' ) ) 703 + '/'; 704 } 705 } 706 return ""; 707 } 708 }, 709 710 importsrc:function ( src, callback, matcher, exclude, win ) { 711 win = win || window; 712 var doc = win.document; 713 714 var srcpath = location.href.split( "/_test/" )[0] 715 + "/_test/tools/br/import.php"; 716 var param0 = src; 717 var ps = { 718 f:src 719 }; 720 if ( exclude ) 721 ps.e = exclude; 722 var param1 = exclude || ""; 723 /** 724 * IE下重复载入会出现无法执行情况 725 */ 726 if ( win.execScript ) { 727 $.get( srcpath, ps, function ( data ) { 728 win.execScript( data ); 729 } ); 730 } else { 731 var head = doc.getElementsByTagName( 'head' )[0]; 732 var sc = doc.createElement( 'script' ); 733 sc.type = 'text/javascript'; 734 sc.src = srcpath + "?f=" + param0 + "&e=" + param1; 735 head.appendChild( sc ); 736 } 737 738 matcher = matcher || src; 739 var mm = matcher.split( "," )[0].split( "." ); 740 var h = setInterval( function () { 741 var p = win; 742 for ( var i = 0; i < mm.length; i++ ) { 743 if ( typeof (p[mm[i]]) == 'undefined' ) { 744 // console.log(mm[i]); 745 return; 746 } 747 p = p[mm[i]]; 748 } 749 clearInterval( h ); 750 if ( callback && 'function' == typeof callback ) 751 callback(); 752 }, 20 ); 753 }, 754 755 /* * 756 用于加载css文件,如果没有加载完毕则不执行回调函数 757 */ 758 loadcss:function ( url, callback, classname, style, value ) { 759 var links = document.getElementsByTagName( 'link' ); 760 for ( var link in links ) { 761 if ( link.href == url ) { 762 callback(); 763 return; 764 } 765 } 766 var head = document.getElementsByTagName( 'head' )[0]; 767 var link = head.appendChild( document.createElement( 'link' ) ); 768 link.setAttribute( "rel", "stylesheet" ); 769 link.setAttribute( "type", "text/css" ); 770 link.setAttribute( "href", url ); 771 var div = document.body.appendChild( document.createElement( "div" ) ); 772 $( document ).ready( 773 function () { 774 div.className = classname || 'cssloaded'; 775 var h = setInterval( function () { 776 if ( $( div ).css( style || 'width' ) == value 777 || $( div ).css( style || 'width' ) == '20px' ) { 778 clearInterval( h ); 779 document.body.removeChild( div ); 780 setTimeout( callback, 20 ); 781 } 782 }, 20 ); 783 } ); 784 }, 785 786 /** 787 * 在一段时间内不断查询oncheck是否返回true,如果是则执行onsuccess,否则超时结束后执行onfail 788 * @param {Function} oncheck 回调函数,定期检查某一个结果是否正确,如果是则返回true 789 * @param {Function} onsuccess,oncheck返回true时执行 790 * @param {Function} onfail 超时结束时执行,用例失败 791 * @param timeout {Number} 超时时间 792 */ 793 delayhelper:function ( oncheck, onsuccess, onfail, timeout ) { 794 onsuccess = onsuccess || oncheck.onsuccess; 795 onfail = onfail || oncheck.onfail || function () { 796 window.QUnit.fail( 'timeout wait for timeout : ' + timeout + 'ms' ); 797 start(); 798 }; 799 timeout = timeout || oncheck.timeout || 10000; 800 801 oncheck = (typeof oncheck == 'function') ? oncheck : oncheck.oncheck; 802 var h1 = setInterval( function () { 803 if ( !oncheck() ) 804 return; 805 else { 806 clearInterval( h1 ); 807 clearTimeout( h2 ); 808 typeof onsuccess == "function" && onsuccess(); 809 } 810 }, 20 ); 811 var h2 = setTimeout( function () { 812 clearInterval( h1 ); 813 clearTimeout( h2 ); 814 onfail(); 815 }, timeout ); 816 }, 817 /** 818 * @name browser 819 * @see UserAction.brower.ie,UserAction.brower.chrome,UserAction.brower.webkit 820 */ 821 browser:(function () { 822 var win = window; 823 824 var numberify = function ( s ) { 825 var c = 0; 826 return parseFloat( s.replace( /\./g, function () { 827 return (c++ == 1) ? '' : '.'; 828 } ) ); 829 }, 830 831 nav = win && win.navigator, 832 833 o = { 834 835 /** 836 * 判断是否为ie并返回ie的版本 837 *@namespace UserAction 838 * @name UserAction.browser.ie 839 * @property ie ie的版本号 840 * @type float 841 * @return {Number} ie版本号 842 * @static 843 */ 844 ie:0, 845 846 /** 847 * 848 * 判断是否为opera并返回opera的版本 849 *@namespace UserAction 850 * @name UserAction.browser.opera 851 * @property opera opera版本号 852 * @type float 853 * @return {Number} opera版本号 854 * @static 855 */ 856 opera:0, 857 858 /** 859 * 是否是Gecko引擎 860 * <pre> 861 * Firefox 1.0.0.4: 1.7.8 <-- Reports 1.7 862 * Firefox 1.5.0.9: 1.8.0.9 <-- 1.8 863 * Firefox 2.0.0.3: 1.8.1.3 <-- 1.81 864 * Firefox 3.0 <-- 1.9 865 * Firefox 3.5 <-- 1.91 866 * </pre> 867 * 868 * 判断是否为gecko 869 *@namespace UserAction 870 * @name UserAction.browser.gecko 871 * @property gecko gecko版本号 872 * @type float 873 * @return {Number} 0或非0 874 * @static 875 */ 876 gecko:0, 877 878 /** 879 * 880 * 判断是否为webkit并返回webkit的版本 881 *@namespace UserAction 882 * @name UserAction.browser.webkit 883 * @property webkit webkit的版本号 884 * @type float 885 * @return {Number} 0或非0 886 * @static 887 */ 888 webkit:0, 889 890 /** 891 * 判断是否为chrome并返回chrome的版本 892 *@namespace UserAction 893 * @name UserAction.browser.chrome 894 * @property chrome chrome版本号 895 * @type float 896 * @return {Number} 0或非0(chrome版本号) 897 * @static 898 */ 899 chrome:0, 900 /** 901 * 判断是否为safari并返回safari的版本 902 *@namespace UserAction 903 * @name UserAction.browser.safari 904 * @property safari safari版本号 905 * @type float 906 * @return {Number} 0或非0(safari版本号) 907 * @static 908 */ 909 safari:0, 910 /** 911 * 判断是否为firefox并返回firefox的版本 912 *@namespace UserAction 913 * @name UserAction.browser.firefox 914 * @property firefox firefoxi版本号 915 * @type float 916 * @return {Number} 0或非0(firefox版本号) 917 * @static 918 */ 919 firefox:0, 920 /** 921 * 判断是否为maxthon并返回maxthon的版本 922 *@namespace UserAction 923 * @name UserAction.browser.firefox 924 * @property maxthon maxthon版本号 925 * @type float 926 * @return {Number} 0或非0(maxthon版本号) 927 * @static 928 */ 929 maxthon:0, 930 /** 931 * 判断是否为maxthon的IE模式 932 *@namespace UserAction 933 * @name UserAction.browser.maxthonIE 934 * @property maxthonIE 是否是遨游的IE模式 935 * @type float 936 * @return {Number} 0或1 937 * @static 938 */ 939 maxthonIE:0, 940 941 /** 942 * Currently limited to Safari on the iPhone/iPod Touch, 943 * Nokia N-series devices with the WebKit-based browser, and Opera 944 * Mini. 945 * 判断是否为移动设备上的浏览器 946 *@namespace UserAction 947 * @name UserAction.browser.mobile 948 * @property mobile 949 * @type string 950 * @static 951 */ 952 mobile:null, 953 954 /** 955 * Adobe AIR version number or 0. Only populated if webkit is 956 * detected. Example: 1.0 957 * 958 * @property air 959 * @type float 960 */ 961 air:0, 962 963 /** 964 * Google Caja version number or 0. 965 * 966 * @property caja 967 * @type float 968 */ 969 caja:nav && nav.cajaVersion, 970 971 /** 972 * Set to true if the pagebreak appears to be in SSL 973 * 974 * @property secure 975 * @type boolean 976 * @static 977 */ 978 secure:false, 979 980 /** 981 * The operating system. Currently only detecting windows or 982 * macintosh 983 * 984 * @property os 985 * @type string 986 * @static 987 */ 988 os:null 989 990 }, 991 992 ua = nav && nav.userAgent, 993 994 loc = win && win.location, 995 996 href = loc && loc.href, 997 998 m; 999 1000 o.secure = href && (href.toLowerCase().indexOf( "https" ) === 0); 1001 1002 if ( ua ) { 1003 1004 if ( (/windows|win32/i).test( ua ) ) { 1005 o.os = 'windows'; 1006 } else if ( (/macintosh/i).test( ua ) ) { 1007 o.os = 'macintosh'; 1008 } else if ( (/rhino/i).test( ua ) ) { 1009 o.os = 'rhino'; 1010 } 1011 1012 // Modern KHTML browsers should qualify as Safari X-Grade 1013 if ( (/KHTML/).test( ua ) ) { 1014 o.webkit = 1; 1015 } 1016 if ( window.external && /(\d+\.\d)/.test( external.max_version ) ) { 1017 1018 o.maxthon = parseFloat( RegExp['\x241'] ); 1019 if ( /MSIE/.test( ua ) ) { 1020 o.maxthonIE = 1; 1021 o.maxthon = 0; 1022 } 1023 1024 } 1025 // Modern WebKit browsers are at least X-Grade 1026 m = ua.match( /AppleWebKit\/([^\s]*)/ ); 1027 if ( m && m[1] ) { 1028 o.webkit = numberify( m[1] ); 1029 1030 // Mobile browser check 1031 if ( / Mobile\//.test( ua ) ) { 1032 o.mobile = "Apple"; // iPhone or iPod Touch 1033 } else { 1034 m = ua.match( /NokiaN[^\/]*|Android \d\.\d|webOS\/\d\.\d/ ); 1035 if ( m ) { 1036 o.mobile = m[0]; // Nokia N-series, Android, webOS, 1037 // ex: 1038 // NokiaN95 1039 } 1040 } 1041 1042 var m1 = ua.match( /Safari\/([^\s]*)/ ); 1043 if ( m1 && m1[1] ) // Safari 1044 o.safari = numberify( m1[1] ); 1045 m = ua.match( /Chrome\/([^\s]*)/ ); 1046 if ( o.safari && m && m[1] ) { 1047 o.chrome = numberify( m[1] ); // Chrome 1048 } else { 1049 m = ua.match( /AdobeAIR\/([^\s]*)/ ); 1050 if ( m ) { 1051 o.air = m[0]; // Adobe AIR 1.0 or better 1052 } 1053 } 1054 } 1055 1056 if ( !o.webkit ) { // not webkit 1057 // @todo check Opera/8.01 (J2ME/MIDP; Opera Mini/2.0.4509/1316; 1058 // fi; U; 1059 // try get firefox and it's ver 1060 // ssr) 1061 m = ua.match( /Opera[\s\/]([^\s]*)/ ); 1062 if ( m && m[1] ) { 1063 o.opera = numberify( m[1] ); 1064 m = ua.match( /Opera Mini[^;]*/ ); 1065 if ( m ) { 1066 o.mobile = m[0]; // ex: Opera Mini/2.0.4509/1316 1067 } 1068 } else { // not opera or webkit 1069 m = ua.match( /MSIE\s([^;]*)/ ); 1070 if ( m && m[1] ) { 1071 o.ie = numberify( m[1] ); 1072 } else { // not opera, webkit, or ie 1073 m = ua.match( /Gecko\/([^\s]*)/ ); 1074 if ( m ) { 1075 o.gecko = 1; // Gecko detected, look for revision 1076 m = ua.match( /rv:([^\s\)]*)/ ); 1077 if ( m && m[1] ) { 1078 o.gecko = numberify( m[1] ); 1079 } 1080 } 1081 } 1082 } 1083 } 1084 } 1085 1086 return o; 1087 })(), 1088 1089 /** 1090 * 提供队列方式执行用例的方案,接口包括start、add、next,方法全部执行完毕时会启动用例继续执行 1091 */ 1092 functionListHelper:function () { 1093 var check = { 1094 list:[], 1095 start:function () { 1096 var self = this; 1097 $( this ).bind( 'next', function () { 1098 setTimeout( function () {// 避免太深的堆栈 1099 if ( self.list.length == 0 ) 1100 start(); 1101 else 1102 self.list.shift()(); 1103 }, 0 ); 1104 } ); 1105 self.next(); 1106 }, 1107 add:function ( func ) { 1108 this.list.push( func ); 1109 }, 1110 next:function ( delay ) { 1111 var self = this; 1112 if ( delay ) { 1113 setTimeout( function () { 1114 $( self ).trigger( 'next' ); 1115 }, delay ); 1116 } else 1117 $( this ).trigger( 'next' ); 1118 } 1119 }; 1120 return check; 1121 }, 1122 /** 1123 * 获取某一个元素的outerHTML 1124 */ 1125 getHTML:function ( co ) { 1126 var div = document.createElement( 'div' ), h; 1127 if ( !co ) 1128 return 'null'; 1129 div.appendChild( co.cloneNode( true ) ); 1130 h = div.innerHTML.toLowerCase(); 1131 1132 h = h.replace( /[\r\n\t\u200b\ufeff]/g, '' ); // Remove line feeds and tabs 1133 h = h.replace( / (\w+)=([^\"][^\s>]*)/gi, ' $1="$2"' ); // Restore 1134 // attribs on IE 1135 return h; 1136 }, 1137 /** 1138 * 获取某一个元素的innerHTML,去掉了不可见字符 1139 */ 1140 getChildHTML:function ( co ) { 1141 1142 var h = co.innerHTML.toLowerCase(); 1143 1144 h = h.replace( /[\r\n\t\u200b\ufeff]/g, '' ); // Remove line feeds and tabs 1145 h = h.replace( / (\w+)=([^\"][^\s>]*)/gi, ' $1="$2"' ); // Restore attribs on IE 1146 1147 return h.replace( /\u200B/g, '' ); 1148 }, 1149 /** 1150 *获取某一个节点在父节点所有孩子中的顺序 1151 * @param node 需要获取顺序的节点 1152 * @returns {Number} 顺序号,从0开始计数 1153 */ 1154 getIndex:function ( node ) { 1155 var childNodes = node.parentNode.childNodes, i = 0; 1156 while ( childNodes[i] !== node ) 1157 i++; 1158 return i; 1159 }, 1160 /** 1161 *清除不可见字符 1162 * @param node 需要清除不可见文本子节点的节点 1163 */ 1164 manualDeleteFillData:function ( node ) { 1165 var childs = node.childNodes; 1166 for ( var i = 0; i < childs.length; i++ ) { 1167 var fillData = childs[i]; 1168 if ( (fillData.nodeType == 3) && ( fillData.data == domUtils.fillChar ) ) { 1169 domUtils.remove( fillData ); 1170 fillData = null; 1171 1172 } 1173 else 1174 this.manualDeleteFillData( fillData ); 1175 } 1176 1177 1178 }, 1179 /** 1180 * 统一cssFloat和styleFloat 1181 */ 1182 cssStyleToDomStyle:function ( cssName ) { 1183 var test = document.createElement( 'div' ).style, 1184 cssFloat = test.cssFloat != undefined ? 'cssFloat' 1185 : test.styleFloat != undefined ? 'styleFloat' 1186 : 'float', 1187 cache = { 'float':cssFloat }; 1188 1189 function replacer( match ) { 1190 return match.charAt( 1 ).toUpperCase(); 1191 } 1192 1193 // return function( cssName ) { 1194 return cache[cssName] || (cache[cssName] = cssName.replace( /-./g, replacer ) ); 1195 // }; 1196 }, 1197 /** 1198 判断两个元素的所有样式是否相同 1199 *@param elementA 比较的一个元素 1200 * @param elementB 比较的另一个元素 1201 * @returns {Boolean} true或false 1202 */ 1203 isSameStyle:function ( elementA, elementB ) { 1204 var styleA = elementA.style.cssText, 1205 styleB = elementB.style.cssText; 1206 if ( this.browser.ie && this.browser.version == 6 ) { 1207 styleA = styleA.toLowerCase(); 1208 styleB = styleB.toLowerCase(); 1209 } 1210 if ( !styleA && !styleB ) { 1211 return true; 1212 } else if ( !styleA || !styleB ) { 1213 return false; 1214 } 1215 var styleNameMap = {}, 1216 record = [], 1217 exit = {}; 1218 styleA.replace( /[\w-]+\s*(?=:)/g, function ( name ) { 1219 styleNameMap[name] = record.push( name ); 1220 } ); 1221 try { 1222 styleB.replace( /[\w-]+\s*(?=:)/g, function ( name ) { 1223 var index = styleNameMap[name]; 1224 if ( index ) { 1225 // name = this.cssStyleToDomStyle( name ); 1226 if ( elementA.style[name] !== elementB.style[name] ) { 1227 throw exit; 1228 } 1229 record[index - 1] = ''; 1230 } else { 1231 throw exit; 1232 } 1233 } ); 1234 } catch ( ex ) { 1235 if ( ex === exit ) { 1236 return false; 1237 } 1238 } 1239 return !record.join( '' ); 1240 }, 1241 /** 1242 * 判断两个元素的所有属性是否都相同 1243 * @param nodeA 比较的第一个节点 1244 * @param nodeB 比较的另一个节点 1245 * @returns {Boolean} true或false 1246 */ 1247 hasSameAttrs:function ( nodeA, nodeB ) { 1248 if ( nodeA.tagName != nodeB.tagName ) 1249 return 0; 1250 var thisAttribs = nodeA.attributes, 1251 otherAttribs = nodeB.attributes; 1252 if ( thisAttribs.length != otherAttribs.length ) 1253 return 0; 1254 if ( thisAttribs.length == 0 ) 1255 return 1; 1256 var attrA, attrB; 1257 for ( var i = 0; attrA = thisAttribs[i++]; ) { 1258 if ( attrA.nodeName == 'style' ) { 1259 if ( this.isSameStyle( nodeA, nodeB ) ) { 1260 continue 1261 } else { 1262 return 0; 1263 } 1264 } 1265 if ( !ua.browser.ie || attrA.specified ) { 1266 attrB = nodeB.attributes[attrA.nodeName]; 1267 if ( !attrB ) { 1268 return 0; 1269 } 1270 } 1271 return 1; 1272 } 1273 return 1; 1274 }, 1275 1276 flag:true, 1277 /** 1278 *检查两个节点(包含所有子节点)是否具有相同的属性 1279 * @param nodeA 比较的第一个节点 1280 * @param nodeB 比较的另一个节点 1281 * @returns {Boolean} true或false 1282 */ 1283 checkAllChildAttribs:function ( nodeA, nodeB ) { 1284 var k = nodeA.childNodes.length; 1285 if ( k != nodeB.childNodes.length ) 1286 this.flag = false; 1287 if ( !this.flag ) 1288 return this.flag; 1289 while ( k ) { 1290 var tmpNodeA = nodeA.childNodes[k - 1]; 1291 var tmpNodeB = nodeB.childNodes[k - 1]; 1292 k--; 1293 1294 if ( tmpNodeA.nodeType == 3 || tmpNodeB.nodeType == 3 || tmpNodeA.nodeType == 8 || tmpNodeB.nodeType == 8 ) 1295 continue; 1296 if ( !this.hasSameAttrs( tmpNodeA, tmpNodeB ) ) { 1297 this.flag = false; 1298 break; 1299 1300 } 1301 1302 this.checkAllChildAttribs( tmpNodeA, tmpNodeB ); 1303 } 1304 return this.flag; 1305 }, 1306 /** 1307 * 判断两个节点(包括所有子孙节点)所有的属性及style是否都相同 1308 * @param nodeA 比较的第一个节点 1309 * @param nodeB 比较的另一个节点 1310 * @returns {Boolean} true或false 1311 */ 1312 haveSameAllChildAttribs:function ( nodeA, nodeB ) { 1313 this.flag = true; 1314 return this.checkAllChildAttribs( nodeA, nodeB ); 1315 }, 1316 /** 1317 * 查看传入的html是否与传入的元素ele具有相同的style,比较结束会执行QUnit的断言 1318 * @param {String} html 被比较的html字符串 1319 * @param {HTMLElement} doc 当前的document对象 1320 * @param {HTMLElement} ele 要被比较的元素,html与ele的innerHTML比较 1321 * @descript {String} descript 断言的描述性语句 1322 */ 1323 checkHTMLSameStyle:function ( html, doc, ele, descript ) { 1324 var tagEle = doc.createElement( ele.tagName ); 1325 tagEle.innerHTML = html; 1326 /*会有一些不可见字符,在比较前提前删掉*/ 1327 this.manualDeleteFillData( ele ); 1328 ok( this.haveSameAllChildAttribs( ele, tagEle ), descript ); 1329 // ok(this.equalsNode(ele.innerHMTL,html),descript); 1330 }, 1331 1332 /** 1333 * 得到选中区域的文本 1334 * @returns {String} 选中区域的文本 1335 */ 1336 getSelectedText:function () { 1337 if ( window.getSelection ) { 1338 // This technique is the most likely to be standardized. 1339 // getSelection() returns a Selection object, which we do not document. 1340 return window.getSelection().toString(); 1341 } 1342 else if ( document.getSelection ) { 1343 // This is an older, simpler technique that returns a string 1344 return document.getSelection(); 1345 } 1346 else if ( document.selection ) { 1347 // This is the IE-specific technique. 1348 // We do not document the IE selection property or TextRange objects. 1349 return document.selection.createRange().text; 1350 } 1351 }, 1352 /** 1353 * 找到给定元素的左上角和右下角顶点的横坐标和纵坐标 1354 * @param {HTMLElement} oElement 1355 * @return {Array} [startX,startY,endX,endY] 1356 */ 1357 findPosition:function ( oElement ) { 1358 var x2 = 0; 1359 var y2 = 0; 1360 var width = oElement.offsetWidth; 1361 var height = oElement.offsetHeight; 1362 if ( typeof( oElement.offsetParent ) != 'undefined' ) { 1363 for ( var posX = 0, posY = 0; oElement; oElement = oElement.offsetParent ) { 1364 posX += oElement.offsetLeft; 1365 posY += oElement.offsetTop; 1366 } 1367 x2 = posX + width; 1368 y2 = posY + height; 1369 return [ posX, posY , x2, y2]; 1370 1371 } else { 1372 x2 = oElement.x + width; 1373 y2 = oElement.y + height; 1374 return [ oElement.x, oElement.y, x2, y2]; 1375 } 1376 }, 1377 /** 1378 获取元素的float属性,ie下用styleFloat获取,非ie用cssFloat 1379 @param {HTMLElement} ele 要获取float属性的元素 1380 @returns {String} ele的float属性 1381 */ 1382 getFloatStyle:function ( ele ) { 1383 if ( this.browser.ie ) 1384 return ele.style['styleFloat']; 1385 else 1386 return ele.style['cssFloat']; 1387 } 1388 1389 1390 }; 1391 var ua = UserAction; 1392 var upath = ua.commonData.currentPath(); 1393 var cpath = ua.commonData.datadir;